import java.net.*;
import javax.net.ssl.*;
import java.util.*;
import java.io.*;
/**
 * This class is a JSSE client that talks to a server over an SSL
 * connection.  Once a connection is made, the client sends a "message" 
 * (defined as a stream terminated by <LF>) to the server, and reads back
 * the response which is a message sent back by the server.
 * <p>The code to perform the SSL stuff is all in "main" - the use of other
 * methods is limited just to displaying information, so that you should
 * only need to read the "main" code to see what order things are done.
 * <p>Built using JDK1.4 and tested on Windows 2000
 * @author Nick Hudson
 * @version 1.0
 */
public class ExampleSSLClient {

    static Object syncObject = new Object();
    
    /**
     * Report a message to System.out
     * @param doTrace true if you want the message to appear, false otherwise
     * @param message the message to be sent to System.out
     */
    public static void trace(boolean doTrace, String message) 
    {
	if (doTrace) {
	    synchronized(syncObject) {
		System.out.println(message);
	    }
	    
	}
    }

    /**
     * Display usage information
     */    
    public static void usage() 
    {
	trace(true,"\nUsage: java ExampleSSLClient <server> <port> "
			   + "<ciphers> <protocol> [trace]");
	trace(true," server      : the server to talk to");
	trace(true," port        : port number to use");
	trace(true," ciphers     : types of cipher suites to propose :-");
	trace(true,"               'c' : suites that use server-cert");
	trace(true,"               'a' : anonymous (no server-cert)");
	trace(true," protocol    : '1' or '3' where :-");
	trace(true,"               '1' means TLSv1");
	trace(true,"               '3' means SSLv3");
	trace(true," trace       : (optional) tracing to enable:-");
	trace(true,"               'j' : turn on JSSE debug tracing");
	trace(true,"               't' : turn on program tracing");
    }
    
    /**
     * This method looks at the run-time environment in an attempt to check
     * if things may need setting up for JSSE to work properly.  It reports
     * any areas of concern but does not abort.
     */
    public static void checkEnvironment() 
    {
	// Check whether they've specified a trustStore.  If they haven't, then
	// JSSE looks for files jssecacerts or cacerts as specified in the
	// JSSE Reference Guide
	String trustStoreName = System.getProperty("javax.net.ssl.trustStore");
	if (trustStoreName == null) {
	    // Tell them how to set it
	    trace(true,
"\nYou have not specified a truststore, which means that JSSE will look in");
	    trace(true,
"default locations (see JSSE Reference Guide) to validate any certificates");
	    trace(true,
"offered by the server.  To specify a truststore, use");
	    trace(true,
"use e.g. :\n");
	    trace(true,
"  java -Djavax.net.ssl.trustStore=\"<file>\" ...\n");
	}
	
	    
    }    

    /**
     * Display various information out of a HandshakeCompletedEvent object.
     * @param event HandshakeCompletedEvent object.
     */
    public static void showHandshakeInfo(HandshakeCompletedEvent event) 
    {
	// This method may be called from a separate thread, so all of
	// the output here in a synchronized block to make sure it doesn't
	// get interspersed with other messages.
	synchronized(syncObject)
        {    
	    trace(true,"Negotiated cipher suite " + event.getCipherSuite());
	    
	    java.security.cert.Certificate lc[] = event.getLocalCertificates();
	    if (lc != null) {
		trace(true,"" + lc.length + " local certificates used :");
		for (int i=0; i<lc.length; i++) {
		    trace(true,"#" + (i+1) + " " + lc[i].toString());
		}
	    }
	    else {
		trace(true,"No local certificates used");
	    }		
	    
	    try {
		lc = event.getPeerCertificates();
		if (lc != null) {
		    trace(true,"" + lc.length + " peer certificates :");
		    for (int i=0; i<lc.length; i++) {
			trace(true,"#" + (i+1) + " " + lc[i].toString());
		    }
		}
	    }
	    catch (SSLPeerUnverifiedException e) {
		trace(true,"Peer's identity has not been verified");
	    }
	    trace(true,""); // write a blank line
	}
	
    }

    /**
     * Main method
     * @param args java.lang.String[]
     */
    public static void main(String[] args)
    {
	int portNumber;
	InetAddress serverAddr = null;
	boolean enableAnonSuites = false;
	boolean enableCertSuites = false;
	boolean isTracing = false;
	String protocol = null;
	InputStream inStream = null;
	OutputStream outStream = null;

	try {
	    serverAddr = InetAddress.getByName(args[0]);
	    portNumber = Integer.parseInt(args[1]);
	    
	    String cipherOptions = args[2].toLowerCase();
	    enableAnonSuites = (cipherOptions.indexOf("a") != -1);
	    enableCertSuites = (cipherOptions.indexOf("c") != -1);
	    
	    switch (Integer.parseInt(args[3])) {
	    case 1 : protocol = "TLSv1"; break;
	    case 3 : protocol = "SSLv3" ; break;
	    default : usage(); return;
	    }

	    // trace argument is optional
	    if (args.length > 4) {
		String traceOptions = args[4].toLowerCase();
		isTracing = (traceOptions.indexOf("t") != -1);
		if (traceOptions.indexOf("j") != -1) 
		    {
			// Enable JSSE tracing
			trace(isTracing, "Enabling JSSE debug tracing");
			System.setProperty("javax.net.debug","ssl");
		    }
	    }
	}
	catch (Exception e) {
	    trace(true,e.toString());
	    // They invoked the program with invalid arguments
	    usage();
	    return;
	}
	
	checkEnvironment();

	SSLSocketFactory sf = null;
	SSLSocket socket = null;
	
	try {
	    trace(true,"Configuring SSLSocketFactory, please wait...");
	    sf = (SSLSocketFactory)SSLSocketFactory.getDefault();
	    
	    trace(isTracing,"Creating new client Socket");
	    socket = (SSLSocket)sf.createSocket(serverAddr,portNumber);
	    
	    if (isTracing) {
		socket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
		    public void handshakeCompleted(HandshakeCompletedEvent e) 
		    {
			trace(true,"SSL handshake completed : ");
			showHandshakeInfo(e);
		    }
		    });
	    }
	    
	    // Look at each of the supported cipher suites, and build a list
	    // of those that we want to use, based on the user's requirements
	    String[] supportedSuites = socket.getSupportedCipherSuites();
	    HashSet set = new HashSet();
	    
	    trace(isTracing,"Supported cipher suites are:");
	    
	    for (int i=0; i<supportedSuites.length; i++) {
		trace(isTracing,"  " + supportedSuites[i]);
		if (supportedSuites[i].indexOf("anon") != -1) {
		    // This is an anonymous suite
		    if (enableAnonSuites) {
			set.add(supportedSuites[i]);
		    }
		}
		else {
		    // This is not an anonymous suite
		    if (enableCertSuites) {
			set.add(supportedSuites[i]);
		    }
		}
	    }
	    
	    // Build an array of all the suites that we just put on the stack
	    String[] desiredSuites = (String[])set.toArray(new String[0]);
	    socket.setEnabledCipherSuites(desiredSuites);
	    if (isTracing) {
		trace(true,"Enabled cipher suites are:");
		String[] enabledSuites = socket.getEnabledCipherSuites();
		for (int i=0; i<enabledSuites.length; i++) {
		    trace(true,"  " + enabledSuites[i]);
		}
	    }

	    socket.setEnabledProtocols(new String[] {protocol});
	    inStream = socket.getInputStream();
	    outStream = socket.getOutputStream();

	}
	catch (Exception e) {
	    trace(true,"Exception while configuring socket :\n" + e);
	    trace(true,"-- program terminating");
	    return;
	}
	
	// The socket should be ready now
	trace(true,"...Now talking to server");
	
	try {
	    outStream.write("Message sent from JSSE Client\n".getBytes());
	    
	    trace(true,"Message sent to server");
	    // Read the response a byte at a time from the socket, looking for 
	    // an EOL which will terminate the message.  Push each
	    // byte into a ByteArrayOutputStream which we can subsequently
	    // turn into a String
	    
	    ByteArrayOutputStream b = new ByteArrayOutputStream();
	    int oneByte;
	    do {
		oneByte = inStream.read();
		if (oneByte == -1) {
		    throw new SocketException("server closed connection");
		}		
		b.write(oneByte);
	    }
	    while (oneByte != 10);
	    
	    // Turn the stream of bytes into a String
	    String message = b.toString();
	    
	    trace(true,"The server sent -->" + message + "<--\n");

	}
	catch (Exception e) {
	    trace(true,"Exception while communicating with server :\n" + e);
	    trace(true,"-- program terminating");
	    return;
	}

	finally {
	    try {
		// "Socket.close()" requires a catch clause
		if (socket != null) {
		    trace(isTracing,"Closing socket");
		    socket.close();
		}
	    }
	    catch (Exception e) {
		trace(true,"Exception when closing socket:\n" + e.toString());
	    }
	    
	}

    }
}

